/**
 * Copyright (c) 2013, George Zeakis<zeageorge@gmail.com>
 * This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * Project started: 2013/04
 * Version 2.0: 2013/10/12, init version 2.0
 * Version 1.0: 2013/05/01, uploaded on http://www.jsclasses.org/
 */

/**
 * Description:
 * publish-subscribe pattern implementation 
 * with priorities and custom context for each callback.
 * Also supporting interruption using [return false]
 * 
 * 
 * Todo: 
 * 1) Add namespacing for topics
 * 
var subup = new Subup(); or var subup = new Subup(128);

subup.subscribe('test', [
    function(){
       console.log('This is test listener');
    },5,this
]);
Subscribe to 'test' with a callback function, priority of 5 and context of <this>


subup.subscribe('test', [function(){
  console.log('This is another test listener');
}]);
Subscribe to 'test' with a callback function, priority of defaultPriority and context of <window>

subup.publish('test');
Publish 'test' topic-event

subup.publish('test',{v1='test'});
Publish 'test' topic-event and "pass" the second arg (object) as arg to all subscribers' callbacks

Other examples:
var subup = new Subup(100);
    
    function tt(){ this.v1='Zeakis'; };

    //create an object to use as context....
    var to = new tt();

    //subscribe to topic 'test' with default priority (100) and default context (window)
    subup.subscribe('test', [function(d){document.writeln('priority:default,  args: '+((d)?d.toString():'_')+'<br />');}]);

    //subscribe to topic 'test1' with priority=1 and context = [to] object 
    subup.subscribe('test1',[function(d){document.writeln('priority:1a, args: '+((d)?d.toString():'_')+', [context].v1='+this.v1+'<br />');},1,to]);
    
    //subscribe to topics [test6, test7] with 3 callbacks:
    subup.batchSubscribe(['test6','test7'],[
        [
            //callback in priority 4 and context of [this]
            function(d){document.writeln('priority:4,  args: '+((d)?d.toString():'_')+',<br />');},4,this
        ],
        [
            //callback in priority 101 and context of [this]
            function(d){document.writeln('priority:101,  args: '+((d)?d.toString():'_')+',<br />');},101,this
        ],
        [
            //callback in priority 0 and context of [to] object
            function(d){document.writeln('priority:0,  args: '+((d)?d.toString():'_')+',<br />');},0,to
        ],
    ]);
    
    // every time, onBefore execute any callbacks of a topic, run the following callback
    subup.subscribeToAll('before',[
        function(d){
            document.writeln('stiky: this will run onBefore every topic publishing<br />');
        },9,this
    ]);
    
    // every time, onAfter execute any callbacks of a topic, run the following callback
    subup.subscribeToAll('after',[
        function(d){
            document.writeln('stiky: this will run onAfter every topic publishing<br /><br />');
        }
    ]);
    subup.publish('test',{v1:'George Zeakis'});
    subup.publish('test1',{v1:'George Zeakis'});
    subup.publish('test6',{v1:'George Zeakis'});
    subup.publish('test7',{v1:'George Zeakis'});

    subup.batchPublish(['test6','test7'],'whh');
    subup.publishAll('args....');

    console.log(subup.topics);
    console.log(subup.stickies);

 */

/**
 
 */

function Subup(_defaultPriority, _priority_min, _priority_max){
  var callbacksID_PartsSeparator='_';
  var namespaceSeparator = "."; // todo: bale to sto config
  
  this.topics = {};
  this.stickies = {};
  this.defaultPriority=(typeof _defaultPriority==='number')?_defaultPriority:128;
  this.priority_min = (typeof _priority_min==='number' && _priority_min>=0)?_priority_min:0;
  this.priority_max = (typeof _priority_max==='number' && _priority_max>this.priority_min)?_priority_max:256;
  
  /**
   * @param {string} name
   */
  function Topic(name){ this.name = name; this.callbacks = {}; };
  
  /**
   *  This function, when is used with subscribe and batchSubscribe, returns an array of existing topicNames. It does not creates Topics !!!
   *  
   * @param {string} topicName
   * @returns {array || boolean} An array of strings || false on error
   */
  this.topicNameParser = function (topicName){
      /**
       * topicName cases:
       *   0) '*' -> returns an array with all existing topicNames
       *   1) 'topicName' -> returns ['topicName'] if 'topicName' exists in this.topics
       *   2) 'topicName*' -> returns an array with all existing topicNames starting with 'topicName', eg. ['topicName1','topicName2', 'topicNamereds']
       *   3) '*topicName' -> returns an array with all existing topicNames ending with 'topicName',   eg. ['1topicName','2topicName', 'sadtopicName']
       *   4) '*topicName*' -> combination of 2 and 3 cases ,   eg. ['1topicName2','2topicNameS', 'sadtopicNamesdsfd']
       *   5) 'topicName.*'
       *   
       *   Ta topics-subjects 8a mporoun na dhlwnontai ws:
       *   1) subup.subscribe("system")              -> aplo topic-subject
       *      subup.publish("system")                -> aplo topic-subject
       *    
       *    2) "system*"             -> ola ta topics-subjects pou 3ekinane apo th le3h 'system'
       *    3) "system.config"       -> aplo topic-subject pou dhlwnei to topic-subject 'config' tou root topic 'system'
       *                              me auton ton tropo dhmiourgoume namespaces sta topics
       *    4) "system.config*"      -> bl. case 2
       *    5) "system.*.config"     -> ola ta topics-subjects tou root topic 'system' ta opoia periexoun to config ws child
       *    6) "system.*.config*"    -> bl. cases 2 & 5
       *    7) "*system*"             -> ola ta topics-subjects pou periexoun th le3h 'system'
       */
      if (typeof topicName!=='string'){ return false; };
      
      var nsParts = topicName.split(namespaceSeparator);
      return nsParts;
      
  };
  
  function consoleLog(){ if(window.console && window.console.log){window.console.log( Array.prototype.slice.call(arguments) );}; };
  
  /**
   * @param {string} topicName
   * @param {integer} priority
   * @returns {String} form = 'x1_x2_x3_x4', 
   *                      x1=type of the topic (n=normal or s=stiky)
   *                      x2=topicName
   *                      x3=priority number
   *                      x4=random chars
   *                      _ = callbacksID_PartsSeparator (var of the subup object) 
   */
  function getNew_callbackID(topicType, topicName, priority){
      return topicType+callbacksID_PartsSeparator+topicName+callbacksID_PartsSeparator+priority+callbacksID_PartsSeparator+'xxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c){var r = Math.random()*16|0,v=c=='x'?r:r&0x3|0x8;return v.toString(16);});
  };
  
  /**
   * @param {string} topicName
   * @param {anything} eventArgs
   */
  this.publish = function(topicName, eventArgs){
        if (typeof topicName!=='string'){consoleLog('error: A topicName must be type of string'); return false;};
        
        var that=this;
        function runStickies(_topicName, _args){
            if (typeof _topicName!=='string'){consoleLog('error: A topicName must be type of string'); return null; };
            if (!that.stickies[_topicName]){return null;};
            
            var _priorities_array=[];
            for (var prop in that.stickies[_topicName].callbacks){ 
                if (Object.prototype.hasOwnProperty.call(that.stickies[_topicName].callbacks, prop)){ _priorities_array.push(prop); };
            };
            _priorities_array.sort(function(a, b){return Number(a)-Number(b);});

            var j,l=_priorities_array.length;
            for(var j=0; j<l; j++){
                var k, g=that.stickies[_topicName].callbacks[_priorities_array[j]].length;
                for (k=0; k<g; k++){
                    try{
                        if (that.stickies[_topicName].callbacks[_priorities_array[j]][k][0].call(that.stickies[_topicName].callbacks[_priorities_array[j]][k][1],_args)===false){ return false;};
                    } catch(error){ consoleLog('error: A callback failed to run with error:', error); };
                };
            };
        };

        if (runStickies('before', eventArgs)===false){ return false; };
        
        if (this.topics[topicName]){
            var priorities_array=[];
            for (var prop in this.topics[topicName].callbacks){ 
                if (Object.prototype.hasOwnProperty.call(this.topics[topicName].callbacks, prop)){ priorities_array.push(prop); };
            };
            priorities_array.sort(function(a, b){return Number(a)-Number(b);});

            var j,l=priorities_array.length;
            for(var j=0; j<l; j++){
                var k, g=this.topics[topicName].callbacks[priorities_array[j]].length;
                for (k=0; k<g; k++){
                    try{
                        if (this.topics[topicName].callbacks[priorities_array[j]][k][0].call(this.topics[topicName].callbacks[priorities_array[j]][k][1],eventArgs)===false){ return false;};
                    } catch(error){ consoleLog('error: A callback failed to run with error:', error); };
                };
            };
        };
        
        if (runStickies('after', eventArgs)===false){ return false; };
  };

  /**
   * @param {string} topicName
   * @param {array}  callback Array[0]=callback (function), Array[1]=priority (integer), Array[2]=context (object)
   * @returns {boolean || string} false on error or callback id
   */  
  this.subscribe = function(topicName, callback){
      if (typeof topicName!=='string'){consoleLog('error: A topicName must be type of string'); return false; };
      if (Object.prototype.toString.call(callback)!==Object.prototype.toString.call([])){ consoleLog('error: callback must be an array'); return false; };
      if (!this.topics[topicName]){this.topics[topicName] = new Topic(topicName);};

      var prior = (typeof callback[1]==='number' && callback[1]>=this.priority_min && callback[1]<=this.priority_max)?callback[1]:this.defaultPriority;
      if (!this.topics[topicName].callbacks[prior]){ this.topics[topicName].callbacks[prior] = []; };
      
      var callback_id = getNew_callbackID('n',topicName,prior);
      this.topics[topicName].callbacks[prior].push([ callback[0], (typeof callback[2]==='function' || typeof callback[2]==='object')?callback[2]:window, callback_id ]);
      return callback_id;
  };
  
  /**
   * @param {string} callback_id
   * @returns {boolean} false on error, true on success or null if id does not exists
   */
  this.unsubscribe = function(callback_id){
      if (typeof callback_id!=='string'){ return false; };
      
      var parts = callback_id.split(callbacksID_PartsSeparator), container;
      if (parts[0]==='n'){ container = this.topics;
      } else if (parts[0]==='s'){ container = this.stickies;
      } else { return false; };
      
      if (!container[parts[1]] && container[parts[1]].callbacks[parts[2]]){ return null; };
      
      for (var p in container[parts[1]].callbacks[parts[2]]){
          if (Object.prototype.hasOwnProperty.call(container[parts[1]].callbacks[parts[2]], p)){
              var c, l=container[parts[1]].callbacks[parts[2]].length, pos=-1;
              for (c=0; c<l; c++){ if (container[parts[1]].callbacks[parts[2]][c][2]===callback_id){ pos=c; break; }; };
              if (pos>-1){ // if found the wanted callback, delete it...
                  container[parts[1]].callbacks[parts[2]].splice(pos,1);
                  if (container[parts[1]].callbacks[parts[2]].length===0){ //if also, callbacks[priority] has no other callbacks, then remove that priority.
                      container[parts[1]].callbacks[parts[2]]=null;
                      delete container[parts[1]].callbacks[parts[2]];
                  };
                  return true; // You must return true here...
              };
          };
      };
      return null; //leave it here. If id does not exists then return null.
  };
  
  /**
   * @param {array} topicNames Array of strings
   * @param {array} callbacks Array of functions or functions references
   * @returns {Boolean || array} false on error or an array of callbacks ids
   */
  this.batchSubscribe = function(topicNames, callbacks){
      if (Object.prototype.toString.call(topicNames)!==Object.prototype.toString.call([])){ consoleLog('error: topicNames must be an array of strings'); return false; };
      if (Object.prototype.toString.call(callbacks)!==Object.prototype.toString.call([])){ consoleLog('error: callbacks must be an array of arrays'); return false; };
      
      var tn_i, tn_l=topicNames.length, callback_ids=[];
      for (tn_i=0; tn_i<tn_l; tn_i++){
          if (typeof topicNames[tn_i]!=='string'){consoleLog('error: A topicName must be type of string'); continue;};
          
          var tc_i, tc_l=callbacks.length;
          for (tc_i=0; tc_i<tc_l; tc_i++){ callback_ids.push(this.subscribe(topicNames[tn_i],callbacks[tc_i])); };
      };
      return callback_ids; 
  };

  /**
   * @param {array} topicNames Array of strings
   * @param {anything} args
   * @returns {Boolean} True on success, false on error
   */
  this.batchPublish = function(topicNames, args){
      if (Object.prototype.toString.call(topicNames)!==Object.prototype.toString.call([])){ consoleLog('error: topicNames must be an array of strings'); return false; };
      
      var tn_i, tn_l=topicNames.length;
      for (tn_i=0; tn_i<tn_l; tn_i++){
          if (typeof topicNames[tn_i]!=='string'){consoleLog('error: A topicName must be type of string'); continue;};
          this.publish(topicNames[tn_i], args);
      };
      return true; 
  };
  
  /**
   * @param {string} when A string = 'before' || 'after'
   * @param {array} callback Array of the form [0]=callback (function), [1]=priority (integer), [2]=context (object)
   * @returns {Boolean || string} False on error || callback id
   */
  this.subscribeToAll = function(when, callback){
      if (typeof when!=='string'){ return false; };
      if (Object.prototype.toString.call(callback)!==Object.prototype.toString.call([])){ consoleLog('error: callback must be an array'); return false; };
      
      var topicName='';
      if (when==='before'){ topicName='before';
      } else if (when==='after'){ topicName='after';
      } else { return false; };
      
      if (!this.stickies[topicName]){this.stickies[topicName] = new Topic(topicName);};
      var prior = (typeof callback[1]==='number' && callback[1]>=this.priority_min && callback[1]<=this.priority_max)?callback[1]:this.defaultPriority;
      if (!this.stickies[topicName].callbacks[prior]){ this.stickies[topicName].callbacks[prior] = []; };
      
      var callback_id = getNew_callbackID('s',topicName,prior);
      this.stickies[topicName].callbacks[prior].push([ callback[0], (typeof callback[2]==='function' || typeof callback[2]==='object')?callback[2]:window, callback_id ]);
      return callback_id;
  };
  
  /**
   * @param {anything} args
   */
  this.publishAll = function(args){
      for (var tpc in this.topics){ if (Object.prototype.hasOwnProperty.call(this.topics, tpc) && this.topics[tpc] instanceof Topic){ this.publish(tpc,args); }; };
  };
};

var s = new Subup();
console.log("no params : ", s.topicNameParser());
console.log("empty string : ", s.topicNameParser(""));
console.log("just a dot : ", s.topicNameParser('.'));
console.log(".a : ", s.topicNameParser(".a"));
console.log("a. :", s.topicNameParser("a."));
console.log("..a :", s.topicNameParser("..a"));
console.log("a.. :", s.topicNameParser("a.."));
console.log("a.a. :", s.topicNameParser("a.a."));
console.log(".a.a :", s.topicNameParser(".a.a"));

